<?php
/*
PHP-UglifyJS-Compressor is a wrapper for uglify, easy to use uglify cli.
You can use it to process javascript code online use PHP.
@Author     aTool
@Date:      2016-01-17
@Github     https://github.com/hustcc/PHP-UglifyJS-Compressor
@Issue      https://github.com/hustcc/PHP-UglifyJS-Compressor/issues/new

#############################
Compressor Options:
sequences -- join consecutive simple statements using the comma operator
properties -- rewrite property access using the dot notation, for example foo["bar"] → foo.bar
dead_code -- remove unreachable code
drop_debugger -- remove debugger; statements
unsafe (default: false) -- apply "unsafe" transformations (discussion below)
conditionals -- apply optimizations for if-s and conditional expressions
comparisons -- apply certain optimizations to binary nodes, for example: !(a <= b) → a > b (only when unsafe), attempts to negate binary nodes, e.g. a = !b && !c && !d && !e → a=!(b||c||d||e) etc.
evaluate -- attempt to evaluate constant expressions
booleans -- various optimizations for boolean context, for example !!a ? b : c → a ? b : c
loops -- optimizations for do, while and for loops when we can statically determine the condition
unused -- drop unreferenced functions and variables
hoist_funs -- hoist function declarations
hoist_vars (default: false) -- hoist var declarations (this is false by default because it seems to increase the size of the output in general)
if_return -- optimizations for if/return and if/continue
join_vars -- join consecutive var statements
cascade -- small optimization for sequences, transform x, x into x and x = something(), x into x = something()
warnings -- display warnings when dropping unreachable code or unused declarations etc.
negate_iife -- negate "Immediately-Called Function Expressions" where the return value is discarded, to avoid the parens that the code generator would insert.
pure_getters -- the default is false. If you pass true for this, UglifyJS will assume that object property access (e.g. foo.bar or foo["bar"]) doesn't have any side effects.
pure_funcs -- default null. You can pass an array of names and UglifyJS will assume that those functions do not produce side effects. DANGER: will not check if the name is redefined in scope. An example case here, for instance var q = Math.floor(a/b). If variable q is not used elsewhere, UglifyJS will drop it, but will still keep the Math.floor(a/b), not knowing what it does. You can pass pure_funcs: [ 'Math.floor' ] to let it know that this function won't produce any side effect, in which case the whole statement would get discarded. The current implementation adds some overhead (compression will be slower).
drop_console -- default false. Pass true to discard calls to console.* functions.
keep_fargs -- default true. Prevents the compressor from discarding unused function arguments. You need this for code which relies on Function.length.
keep_fnames -- default false. Pass true to prevent the compressor from mangling/discarding function names. Useful for code relying on Function.prototype.name.
#############################
 */
class UglifyJSCompressor {
    
    // absolute path to uglifyjs file, default is `uglifyjs`.
    private $UGLIFY_JS = 'uglifyjs';
    private $TEMP_FILES_DIR;
    private $options = array(
     'sequences' => true,
     'properties' => true,
     'dead_code' => true,
     'drop_debugger' => true,
     'unsafe' => false,

     'conditionals' => true,
     'comparisons' => true,
     'evaluate' => true,
     'booleans' => true,
     'loops' => true,
     'unused' => true,

     'hoist_funs' => true,
     'hoist_vars' => false,
     'if_return' => true,
     'join_vars' => true,
     'cascade' => true,
     'warnings' => true,

     'negate_iife' => false,
     
     
     'pure_getters' => false,
     'pure_funcs' => null,
     'drop_console' => false,

     'keep_fargs' => false,
     'keep_fnames' => false
    );
    private $files = array();
    private $string = '';

    private $cmd = null;
    private $scriptFile = null;
    private $status = array('orgin' => 0, 'compress' => 0, 'rate' => 0.0); #compression status, include origin src size, compressed size, rate
    // construct with a path to the YUI jar and a path to a place to put temporary files
    function __construct($UGLIFY_JS, $TEMP_FILES_DIR, $options = array()) {
        $this->UGLIFY_JS = $UGLIFY_JS;
        $this->TEMP_FILES_DIR = $TEMP_FILES_DIR;
        
        foreach ($options as $option => $value) {
            $this->setOption($option, $value);
        }
    }
    // reset all the var, used to a new compress
    function reset() {
        $this->files = array();
        $this->string = '';
        $this->cmd = null;
        $this->scriptFile = null;
    }
    // set one of the YUI compressor options
    function setOption($option, $value) {
        $this->options[$option] = $value;
    }

    // add a file (absolute path) to be compressed
    function addFile($file) {
        array_push($this->files, $file);
    }
    
    // add a strong to be compressed
    function addString($string) {
        $this->string .= ' ' . $string;
    }
    
    function getInputScript() {
        if ($this->scriptFile == null) {
            foreach ($this->files as $file) {
                $this->string .= file_get_contents($file) or die("Cannot read from uploaded file");        
            }
            
            // create single file from all input
            $input_hash = sha1($this->string);
            $this->scriptFile = $this->TEMP_FILES_DIR . '/' . $input_hash . '.txt';
            $fh = fopen($this->scriptFile, 'w') or die("Can't create new file");
            fwrite($fh, $this->string);
            fclose($fh);

            $this->status['orgin'] = strlen($this->string);
        }
        return $this->scriptFile;
    }
    // get the uglify cmd
    function getCommand() {
        if ($this->cmd == null) {
            $this->cmd = escapeshellarg($this->UGLIFY_JS) . ' ' . $this->getInputScript() . ' -m -c ';
            $skip = null;
            $params = array();
            foreach ($this->options as $option => $value) {
                $skip = false;
                if ($value === true) $value = 'true';
                else if($value === false) $value = 'false';
                else if($value === null) $value = 'null';
                else $skip = true;
                if (! $skip)
                    $params[] = $option . '=' . $value;
            }
            $params = join(',', $params);
            $this->cmd .= $params;
        }
        return $this->cmd;
    }

    // the meat and potatoes, executes the compression command in shell
    function compress() {
        // read the input
        $this->getInputScript();
        
        // start with command
        $this->getCommand();
    
        // execute the command
        exec($this->cmd . ' 2>&1', $raw_output);
        
        // add line breaks to show errors in an intelligible manner
        $flattened_output = implode("\n", $raw_output);
        
        // clean up (remove temp file)
        unlink($this->scriptFile);
        // save compressed size
        $this->status['compress'] = strlen($flattened_output);
        if ($this->status['orgin'] == 0) $this->status['rate'] = 0.0;
        else $this->status['rate'] = $this->status['compress'] * 1.0 / $this->status['orgin'];
        // return compressed output
        return $flattened_output;
    }
    // get the compress status
    function status() {
        return $this->status;
    }
}